Skip to content

VitePress + GitHub Actions:私有笔记仓库自动发布到 GitHub Pages ​

Markdown 笔记放在私有仓库里,希望自动构建成静态站点发布到 GitHub Pages:源文件保密、只公开构建产物、推送即部署。本文给出一套完整方案,覆盖 VitePress 配置、自动侧边栏生成、GitHub Actions 跨仓库部署与常见问题。适合想把已有笔记仓库变成个人站点的开发者,读完可以直接照配出一个「推送即发布」的笔记站点。

方案架构 ​

私有仓库只存放源文件,构建在 GitHub Actions 里完成,产物推送到公开仓库,由 GitHub Pages 对外发布:

这个架构有几个特点:

  • 源文件保密:私有仓库不对外可见,只有构建结果公开;
  • 自动部署:推送代码即触发构建和发布,无需本地操作;
  • 选择性发布:私密目录(草稿、工作记录等)可以通过配置排除在构建之外;
  • 内置能力:VitePress 自带本地全文搜索、响应式布局与自动生成的侧边栏。

环境要求 ​

  • Node.js:官方文档当前要求 22 及以上(稳定版 1.6.4;早期 1.x 版本要求 18 以上,如果 package.json 里写的是 ^1.0.0,实际会安装到 1.6.x,因此新搭建建议直接用 22 LTS);
  • npm:9.0.0 及以上;
  • Git:2.0 及以上。
bash
node -v
npm -v
git -v

Node.js 多版本切换可以参考 nvm、n 与 Volta 三类版本管理器,此处不展开。

项目配置 ​

目录结构 ​

一个可用的笔记仓库大致长这样:

note-repo/
├── .github/
│   └── workflows/
│       └── deploy.yml            # GitHub Actions 部署配置
├── .vitepress/
│   ├── config.mts                # VitePress 主配置
│   ├── utils/
│   │   └── sidebar.ts            # 自动侧边栏生成工具
│   ├── dist/                     # 构建输出目录(自动生成)
│   └── cache/                    # 缓存目录(自动生成)
├── notes/                        # 内容目录,每个目录放一个 index.md
├── draft/                        # 私密目录(构建时排除)
├── pic/                          # 图片资源目录
├── index.md                      # 网站首页
├── package.json
└── .gitignore

package.json ​

json
{
  "name": "note",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "docs:dev": "vitepress dev",
    "docs:build": "vitepress build",
    "docs:preview": "vitepress preview"
  },
  "devDependencies": {
    "vitepress": "^1.0.0"
  },
  "engines": {
    "node": ">=22.0.0",
    "npm": ">=9.0.0"
  }
}

type: "module" 启用 ES 模块支持,engines 声明版本要求,scripts 定义三个常用命令。

.vitepress/config.mts ​

typescript
import { defineConfig } from 'vitepress'
import { fileURLToPath, URL } from 'node:url'
import { generateSidebar } from './utils/sidebar'

// .vitepress 目录路径
const rootDir = fileURLToPath(new URL('.', import.meta.url))

export default defineConfig({
  title: '个人笔记',
  description: '记录工作学习笔记',
  lang: 'zh-CN',

  // 部署到 https://username.github.io/ 用 '/';
  // 部署到 https://username.github.io/repo/ 用 '/repo/'
  base: '/',

  // 笔记互链常有站外链接,关掉死链检查避免构建失败
  ignoreDeadLinks: true,
  lastUpdated: true,

  // 私密目录不参与构建
  srcExclude: ['**/draft/**', '**/node_modules/**'],

  themeConfig: {
    nav: [
      { text: '首页', link: '/' },
      { text: '笔记', link: '/notes/' }
    ],

    // 侧边栏按目录自动生成
    sidebar: {
      '/notes/': [
        {
          text: '笔记',
          items: generateSidebar('notes', rootDir)
        }
      ]
    },

    // 本地全文搜索(中文化)
    search: {
      provider: 'local',
      options: {
        locales: {
          root: {
            translations: {
              button: { buttonText: '搜索笔记', buttonAriaLabel: '搜索笔记' },
              modal: {
                noResultsText: '无法找到相关结果',
                resetButtonTitle: '清除查询条件',
                footer: { selectText: '选择', navigateText: '切换' }
              }
            }
          }
        }
      }
    },

    lastUpdated: { text: '最后更新于' },
    docFooter: { prev: '上一页', next: '下一页' },
    outline: { label: '页面导航', level: [2, 3] },
    returnToTopLabel: '返回顶部',
    sidebarMenuLabel: '菜单',
    darkModeSwitchLabel: '外观'
  },

  markdown: {
    lineNumbers: true,
    image: { lazyLoading: true }
  }
})

几个选项值得说明:

  • base 决定资源引用前缀,用户名站点(username.github.io)用 /,项目站点(username.github.io/repo/)必须写 /repo/,否则样式与图片全部 404;
  • ignoreDeadLinks 建议开启,笔记里的相对链接经常在构建时被判为死链;
  • srcExclude 用 glob 语法排除私密目录,这是「源仓库可以放任何东西、站点只发布选定部分」的关键;
  • search.provider: 'local' 是 VitePress 内置的本地索引搜索,不需要接 Algolia 等外部服务。

自动侧边栏 ​

侧边栏如果手写,每加一篇笔记都要改配置。可以用一个递归函数在构建时扫描目录自动生成:

typescript
// .vitepress/utils/sidebar.ts
import { readdirSync, statSync } from 'fs'
import { join } from 'path'

export interface SidebarItem {
  text: string
  link?: string
  items?: SidebarItem[]
  collapsed?: boolean
}

function walk(dir: string, linkBase: string): SidebarItem[] {
  const items: SidebarItem[] = []
  const files = readdirSync(dir)

  // 目录在前、文件在后,同级按中文名排序
  files.sort((a, b) => {
    const aDir = statSync(join(dir, a)).isDirectory()
    const bDir = statSync(join(dir, b)).isDirectory()
    if (aDir !== bDir) return aDir ? -1 : 1
    return a.localeCompare(b, 'zh-CN')
  })

  for (const file of files) {
    // 跳过隐藏文件与下划线开头的文件
    if (file.startsWith('.') || file.startsWith('_')) continue
    const full = join(dir, file)
    if (statSync(full).isDirectory()) {
      const sub = walk(full, `${linkBase}/${file}`)
      if (sub.length > 0) items.push({ text: file, collapsed: true, items: sub })
    } else if (file.endsWith('.md') && file !== 'index.md') {
      items.push({ text: file.replace('.md', ''), link: `${linkBase}/${file}` })
    }
  }
  return items
}

// dir:相对站点根的目录名;rootDir:.vitepress 目录
export function generateSidebar(dir: string, rootDir: string): SidebarItem[] {
  return walk(join(rootDir, '..', dir), `/${dir}`)
}

子目录会折叠展示,index.md 被跳过(它承担目录页职责,不进列表)。目录层级变化时侧边栏自动跟上,不需要维护任何清单。

首页与目录页 ​

首页 index.md 用 VitePress 的 home 布局:

markdown
---
layout: home

hero:
  name: "个人笔记"
  text: "记录工作学习的点点滴滴"
  tagline: 持续学习,不断进步
  actions:
    - theme: brand
      text: 开始阅读
      link: /notes/
---

## 关于本站

个人学习笔记站点,使用 VitePress 构建。

每个内容目录放一个 index.md(可以只有一行标题),否则访问该目录路径会 404。

自动化部署 ​

创建 Personal Access Token ​

构建产物要推送到另一个公开仓库,需要给 Actions 授权:

  1. GitHub Settings → Developer settings → Personal access tokens → Tokens (classic);
  2. Generate new token (classic),勾选 repo 完整权限,设置较长有效期;
  3. 生成后立即复制,token 只显示一次。

配置 Secret ​

在私有仓库的 Settings → Secrets and variables → Actions 中新建 secret,名称 PERSONAL_ACCESS_TOKEN,值为刚才的 token。secret 注入环境变量时对日志脱敏,不会泄漏。

工作流 ​

yaml
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [master]
  workflow_dispatch:    # 支持手动触发

permissions:
  contents: read

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 完整历史,用于 lastUpdated 时间

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build with VitePress
        run: npm run docs:build
        env:
          NODE_ENV: production

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          personal_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          external_repository: username/username.github.io  # 目标公开仓库
          publish_branch: main
          publish_dir: .vitepress/dist
          user_name: 'github-actions[bot]'
          user_email: 'github-actions[bot]@users.noreply.github.com'
          force_orphan: true  # 每次全量覆盖,不带历史

要点:

  • permissions: contents: read 最小化本仓库权限,跨仓库推送靠 PAT 完成;
  • node-version: 22 与前述 VitePress 版本要求对应;
  • force_orphan: true 让公开仓库始终只有一次提交,历史不会膨胀。

配置 GitHub Pages ​

在公开仓库(username.github.io)的 Settings → Pages 中,Source 选 Deploy from a branch,Branch 选 main、目录 / (root)。等待一两分钟即可通过 https://username.github.io/ 访问。

本地开发 ​

bash
git clone https://github.com/username/note-repo.git
cd note-repo
npm install
npm run docs:dev      # 开发模式,默认 http://localhost:5173
npm run docs:build    # 构建到 .vitepress/dist
npm run docs:preview  # 本地预览构建结果

本地预览通过后再推送,推送后 Actions 自动完成发布。

常见问题 ​

构建报 Duplicate attribute。 多数是 Markdown 文件的 front matter 开头多了空行——--- 必须是文件第一行。批量排查:

bash
grep -rln $'^---\n\n' --include="*.md" .

访问目录 404。 该目录缺少 index.md,补一个即可。

部署报 Permission denied(git exit code 128)。 PAT 过期或权限不足:确认 token 未过期、勾选了 repo 权限,必要时重新生成并更新 secret。

代码块语言不识别。 VitePress 内置 Shiki 不认识 plantuml 等语言时会回退为纯文本并告警,把语言标注换成 text 或 mermaid 即可。

私密目录出现在站点上。 检查 srcExclude 的 glob 是否覆盖了该目录(注意 **/draft/** 与 draft/** 的区别),改完清缓存重新构建:

bash
rm -rf .vitepress/cache .vitepress/dist
npm run docs:build

小结 ​

这套方案的核心是职责分离:私有仓库管源文件,Actions 管构建,公开仓库只承载产物。配置上的三个关键点是 base 与站点路径的对应、srcExclude 排除私密内容、PAT 驱动的跨仓库推送;自动侧边栏则把目录维护成本降到了零。搭好之后,写笔记、提交、推送,站点自动更新。

最近更新

基于 VitePress 构建